package com.meshnetics.gb.type.impl.custom;

import com.meshnetics.gb.type.DataTypeID;
import com.meshnetics.gb.type.DataValue;
import com.meshnetics.gb.type.impl.ByteUtils;
import com.meshnetics.gb.type.impl.StringUtils;
import com.meshnetics.gb.type.impl.zcl.TypeEnumeration;
import com.meshnetics.gb.type.impl.zcl.FixedBytesDataValue;

import java.util.Arrays;
import java.util.HashMap;

/**
 * Finite enumeration type. Enumeration element is known as its code and have additional string label assigned to it.
 * Only "known" elements can be deserialized - these values must be defined and registered before usage of
 * enumeration type (expected as static public final fields of this class or enumeration element class).
 *
 * @author AKovylin
 */
public abstract class FiniteEnumeration extends TypeEnumeration {
    private final class BytesWrap {
        private final byte[] bytes;
        private final int hashCode;

        public BytesWrap(byte[] bytes) {
            this.bytes = bytes;
            hashCode = Arrays.hashCode(bytes);
        }

        public int hashCode() {
            return hashCode;
        }

        public boolean equals(Object obj) {
            return obj instanceof BytesWrap && Arrays.equals(bytes, ((BytesWrap) obj).bytes);
        }
    }

    private final HashMap values = new HashMap(); // BytesWrap -> FiniteEnumerationElement

    public FiniteEnumeration(String enumName) {
        this(new String[]{enumName}, null, 1);
    }

    public FiniteEnumeration(String enumName, int byteSize) {
        this(new String[]{enumName}, null, byteSize);
    }

    public FiniteEnumeration(String enumName, DataTypeID dataTypeId, int byteSize) {
        this(new String[]{enumName}, dataTypeId, byteSize);
    }

    public FiniteEnumeration(String[] aliases, DataTypeID dataTypeId, int byteSize) {
        super(aliases, dataTypeId, byteSize);
        setFormatter(new FiniteEnumerationmandatoryFormatter(this));
    }

    void registerValue(FiniteEnumerationElement element) {
        // as registerValue has no access modifier - it is used only in constructor of value
        BytesWrap key = new BytesWrap(element.getInnerBytesUnsafe());
        if (values.containsKey(key)) {
            if (!((FiniteEnumerationElement) values.get(key))
                    .getName().equals(element.getName())) {
                throw new IllegalArgumentException("non-equal element already registered");
            }
        } else {
            values.put(key, element);
        }
    }

    // no byte array cloning needed
    protected FixedBytesDataValue createValue(byte[] bytes) {
        // if not registered - error
        // if registered - take name
        FiniteEnumerationElement element = (FiniteEnumerationElement) values.get(new BytesWrap(bytes));
        if (element == null) {
            throw new IllegalArgumentException("enumeration doesn't have element with code " +
                    StringUtils.toHexString(bytes));
        } else {
            return element;
        }
    }

    public FiniteEnumerationElement fromCode(byte code) {
        return fromCode(new byte[]{code});
    }

    public FiniteEnumerationElement fromCode(long code) {
        byte[] bCode = new byte[byteSize];
        ByteUtils.fillFromLong(code, bCode, 0, byteSize);
        return fromCode(bCode);
    }

    public FiniteEnumerationElement fromCode(byte[] bCode) {
        // find and return, exception if doesn't exist
        FiniteEnumerationElement element = (FiniteEnumerationElement) values.get(new BytesWrap(bCode));
        if (element == null) {
            throw new IllegalArgumentException("enumeration doesn't have element with code " +
                    StringUtils.toHexString(bCode));
        } else {
            return element;
        }
    }

    protected class FiniteEnumerationmandatoryFormatter extends FixedBytesMandatoryFormatter {
        public FiniteEnumerationmandatoryFormatter(FiniteEnumeration valueType) {
            super(valueType);
        }

        public String formatShortLogString(DataValue value) {
            checkValue(value);
            FiniteEnumerationElement element = (FiniteEnumerationElement) value;
            return FiniteEnumeration.this.getName() + '(' + StringUtils.toHexString(element.getInnerBytesUnsafe()) +
                    ',' + element.getName() + ')';
        }

        public String formatMaxLogString(DataValue value) {
            checkValue(value);
            FiniteEnumerationElement element = (FiniteEnumerationElement) value;
            return element.getClass().toString() + '(' +
                    element.getType().getClass().toString() + '(' + element.getType().getName() + ')' + ',' +
                    StringUtils.toHexString(element.getInnerBytesUnsafe()) +
                    ',' + element.getName() + ')';
        }
    }
}
